/*-------------------------------------------------------------------------
 *
 * reinit.c
 *	  对未记录关系的重新初始化
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *	  src/backend/storage/file/reinit.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include <unistd.h>

#include "common/relpath.h"
#include "postmaster/startup.h"
#include "storage/copydir.h"
#include "storage/fd.h"
#include "storage/reinit.h"
#include "utils/hsearch.h"
#include "utils/memutils.h"

static void fc_ResetUnloggedRelationsInTablespaceDir(const char *fc_tsdirname,
												  int fc_op);
static void fc_ResetUnloggedRelationsInDbspaceDir(const char *fc_dbspacedirname,
											   int fc_op);

typedef struct
{
	Oid			reloid;			/* 哈希键 */
} unlogged_relation_entry;

/* 重置上次重启前的未记录关系。 */ 
 /* 如果操作包括 UNLOGGED_RELATION_CLEANUP，我们将移除任何关系的所有分叉，除了“init”分叉本身。 */ 
 /* 如果操作包括 UNLOGGED_RELATION_INIT，我们将“init”分叉复制到主分叉。 */
void ResetUnloggedRelations(int fc_op)
{
	char		fc_temp_path[MAXPGPATH + 10 + sizeof(TABLESPACE_VERSION_DIRECTORY)];
	DIR		   *fc_spc_dir;
	struct dirent *fc_spc_de;
	MemoryContext fc_tmpctx,
				fc_oldctx;

	/* Log it. */
	elog(DEBUG1, "resetting unlogged relations: cleanup %d init %d",
		 (fc_op & UNLOGGED_RELATION_CLEANUP) != 0,
		 (fc_op & UNLOGGED_RELATION_INIT) != 0);

	/* 为了确保我们不会泄漏任何内存，让我们为此操作创建一个临时内存上下文。 */
	fc_tmpctx = AllocSetContextCreate(CurrentMemoryContext,
								   "ResetUnloggedRelations",
								   ALLOCSET_DEFAULT_SIZES);
	fc_oldctx = MemoryContextSwitchTo(fc_tmpctx);

	/* 准备报告重置未记录关系的进度。 */
	begin_startup_progress_phase();

	/* 首先处理 pg_default 中的未记录文件 ($PGDATA/base) */
	fc_ResetUnloggedRelationsInTablespaceDir("base", fc_op);

	/* 循环遍历所有非默认表空间的目录。 */
	fc_spc_dir = AllocateDir("pg_tblspc");

	while ((fc_spc_de = ReadDir(fc_spc_dir, "pg_tblspc")) != NULL)
	{
		if (strcmp(fc_spc_de->d_name, ".") == 0 ||
			strcmp(fc_spc_de->d_name, "..") == 0)
			continue;

		snprintf(fc_temp_path, sizeof(fc_temp_path), "pg_tblspc/%s/%s",
				 fc_spc_de->d_name, TABLESPACE_VERSION_DIRECTORY);
		fc_ResetUnloggedRelationsInTablespaceDir(fc_temp_path, fc_op);
	}

	FreeDir(fc_spc_dir);

	/* 恢复内存上下文。 */
	MemoryContextSwitchTo(fc_oldctx);
	MemoryContextDelete(fc_tmpctx);
}

/* 处理 ResetUnloggedRelations 的一个表空间目录 */
static void fc_ResetUnloggedRelationsInTablespaceDir(const char *fc_tsdirname, int fc_op)
{
	DIR		   *fc_ts_dir;
	struct dirent *fc_de;
	char		fc_dbspace_path[MAXPGPATH * 2];

	fc_ts_dir = AllocateDir(fc_tsdirname);

	/* 如果我们在表空间目录中遇到 ENOENT，记录并返回。 */ 
 /* 这可能发生在之前的 DROP TABLESPACE 在删除表空间目录和在 pg_tblspc 中删除符号链接之间崩溃的情况下。 */ 
 /* 在这种情况下，我们并不想阻止数据库启动，所以让它通过。 */ 
 /* 任何其他类型的错误将由 ReadDir 报告（导致启动失败）。 */
	if (fc_ts_dir == NULL && errno == ENOENT)
	{
		ereport(LOG,
				(errcode_for_file_access(),
				 errmsg("could not open directory \"%s\": %m",
						fc_tsdirname)));
		return;
	}

	while ((fc_de = ReadDir(fc_ts_dir, fc_tsdirname)) != NULL)
	{
		/*
		 * 我们只对具有数字名称的每个数据库目录感兴趣。
		 * 请注意，这段代码也会（正确地）忽略 "."
		 * 和 ".."。
		 */
		if (strspn(fc_de->d_name, "0123456789") != strlen(fc_de->d_name))
			continue;

		snprintf(fc_dbspace_path, sizeof(fc_dbspace_path), "%s/%s",
				 fc_tsdirname, fc_de->d_name);

		if (fc_op & UNLOGGED_RELATION_INIT)
			ereport_startup_progress("resetting unlogged relations (init), elapsed time: %ld.%02d s, current path: %s",
									 fc_dbspace_path);
		else if (fc_op & UNLOGGED_RELATION_CLEANUP)
			ereport_startup_progress("resetting unlogged relations (cleanup), elapsed time: %ld.%02d s, current path: %s",
									 fc_dbspace_path);

		fc_ResetUnloggedRelationsInDbspaceDir(fc_dbspace_path, fc_op);
	}

	FreeDir(fc_ts_dir);
}

/* 处理 ResetUnloggedRelations 的每个数据库空间目录 */
static void fc_ResetUnloggedRelationsInDbspaceDir(const char *fc_dbspacedirname, int fc_op)
{
	DIR		   *fc_dbspace_dir;
	struct dirent *fc_de;
	char		fc_rm_path[MAXPGPATH * 2];

	/* 调用者必须指定至少一个操作。 */
	Assert((fc_op & (UNLOGGED_RELATION_CLEANUP | UNLOGGED_RELATION_INIT)) != 0);

	/* 清理是一个两遍操作。首先，我们遍历并识别所有具有 init 分叉的文件。 */ 
 /* 然后，我们再次遍历并删除具有相同 OID 的所有内容，除了 init 分叉。 */
	if ((fc_op & UNLOGGED_RELATION_CLEANUP) != 0)
	{
		HTAB	   *fc_hash;
		HASHCTL		fc_ctl;

		/* 有可能有人在同一个数据库和表空间中创建大量未记录关系，因此我们最好使用哈希表而不是数组或链表来跟踪需要重置的文件。 */ 
 /* 否则，这个清理操作将是 O(n^2)。 */
		fc_ctl.keysize = sizeof(Oid);
		fc_ctl.entrysize = sizeof(unlogged_relation_entry);
		fc_ctl.hcxt = CurrentMemoryContext;
		fc_hash = hash_create("unlogged relation OIDs", 32, &fc_ctl,
						   HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);

		/* 扫描目录。 */
		fc_dbspace_dir = AllocateDir(fc_dbspacedirname);
		while ((fc_de = ReadDir(fc_dbspace_dir, fc_dbspacedirname)) != NULL)
		{
			ForkNumber	fc_forkNum;
			int			fc_oidchars;
			unlogged_relation_entry fc_ent;

			/* 跳过任何看起来不像关系数据文件的东西。 */
			if (!parse_filename_for_nontemp_relation(fc_de->d_name, &fc_oidchars,
													 &fc_forkNum))
				continue;

			/* 也跳过它，除非这是 init 分叉。 */
			if (fc_forkNum != INIT_FORKNUM)
				continue;

			/* 如果名称的 OID 部分还没有放入哈希表中，就把它放进去。 */
			fc_ent.reloid = atooid(fc_de->d_name);
			(void) hash_search(fc_hash, &fc_ent, HASH_ENTER, NULL);
		}

		/* 第一次遍历完成。 */
		FreeDir(fc_dbspace_dir);

		/* 如果我们没有找到任何 init 分叉，那就没有必要继续；我们可以现在退出。 */
		if (hash_get_num_entries(fc_hash) == 0)
		{
			hash_destroy(fc_hash);
			return;
		}

		/* 现在，进行第二次遍历，移除任何匹配的内容。 */
		fc_dbspace_dir = AllocateDir(fc_dbspacedirname);
		while ((fc_de = ReadDir(fc_dbspace_dir, fc_dbspacedirname)) != NULL)
		{
			ForkNumber	fc_forkNum;
			int			fc_oidchars;
			unlogged_relation_entry fc_ent;

			/* 跳过任何看起来不像关系数据文件的东西。 */
			if (!parse_filename_for_nontemp_relation(fc_de->d_name, &fc_oidchars,
													 &fc_forkNum))
				continue;

			/* 我们从不删除 init 分叉。 */
			if (fc_forkNum == INIT_FORKNUM)
				continue;

			/* 查看名称的 OID 部分是否出现在哈希表中。如果是，删除它！ */
			fc_ent.reloid = atooid(fc_de->d_name);
			if (hash_search(fc_hash, &fc_ent, HASH_FIND, NULL))
			{
				snprintf(fc_rm_path, sizeof(fc_rm_path), "%s/%s",
						 fc_dbspacedirname, fc_de->d_name);
				if (unlink(fc_rm_path) < 0)
					ereport(ERROR,
							(errcode_for_file_access(),
							 errmsg("could not remove file \"%s\": %m",
									fc_rm_path)));
				else
					elog(DEBUG2, "unlinked file \"%s\"", fc_rm_path);
			}
		}

		/* 清理完成。 */
		FreeDir(fc_dbspace_dir);
		hash_destroy(fc_hash);
	}

	/* 初始化发生在清理完成之后：我们将每个 init 分叉文件复制到相应的主分叉文件。 */ 
 /* 请注意，如果我们被要求同时进行清理和初始化，我们可能永远不会到这里：如果清理代码确定这个数据库空间中没有 init 分叉，它将在到达这一点之前返回。 */
	if ((fc_op & UNLOGGED_RELATION_INIT) != 0)
	{
		/* 扫描目录。 */
		fc_dbspace_dir = AllocateDir(fc_dbspacedirname);
		while ((fc_de = ReadDir(fc_dbspace_dir, fc_dbspacedirname)) != NULL)
		{
			ForkNumber	fc_forkNum;
			int			fc_oidchars;
			char		fc_oidbuf[OIDCHARS + 1];
			char		fc_srcpath[MAXPGPATH * 2];
			char		fc_dstpath[MAXPGPATH];

			/* 跳过任何看起来不像关系数据文件的东西。 */
			if (!parse_filename_for_nontemp_relation(fc_de->d_name, &fc_oidchars,
													 &fc_forkNum))
				continue;

			/* 也跳过它，除非这是 init 分叉。 */
			if (fc_forkNum != INIT_FORKNUM)
				continue;

			/* 构造源路径名。 */
			snprintf(fc_srcpath, sizeof(fc_srcpath), "%s/%s",
					 fc_dbspacedirname, fc_de->d_name);

			/* 构造目标路径名。 */
			memcpy(fc_oidbuf, fc_de->d_name, fc_oidchars);
			fc_oidbuf[fc_oidchars] = '\0';
			snprintf(fc_dstpath, sizeof(fc_dstpath), "%s/%s%s",
					 fc_dbspacedirname, fc_oidbuf, fc_de->d_name + fc_oidchars + 1 +
					 strlen(forkNames[INIT_FORKNUM]));

			/* 好的，我们准备执行实际的复制。 */
			elog(DEBUG2, "copying %s to %s", fc_srcpath, fc_dstpath);
			copy_file(fc_srcpath, fc_dstpath);
		}

		FreeDir(fc_dbspace_dir);

		/* 
		 * copy_file() 已经对其创建的文件调用了 pg_flush_data()。
		 * 现在我们需要 fsync 这些文件，因为在恢复期间检查点
		 * 并不会为我们完成这项工作。我们在单独的传递中执行此操作
		 * 以允许内核一次性执行所有的刷新
		 * （特别是元数据刷新）。
		 */
		fc_dbspace_dir = AllocateDir(fc_dbspacedirname);
		while ((fc_de = ReadDir(fc_dbspace_dir, fc_dbspacedirname)) != NULL)
		{
			ForkNumber	fc_forkNum;
			int			fc_oidchars;
			char		fc_oidbuf[OIDCHARS + 1];
			char		fc_mainpath[MAXPGPATH];

			/* 跳过任何看起来不像关系数据文件的东西。 */
			if (!parse_filename_for_nontemp_relation(fc_de->d_name, &fc_oidchars,
													 &fc_forkNum))
				continue;

			/* 也跳过它，除非这是 init 分叉。 */
			if (fc_forkNum != INIT_FORKNUM)
				continue;

			/* 构造主分叉路径名。 */
			memcpy(fc_oidbuf, fc_de->d_name, fc_oidchars);
			fc_oidbuf[fc_oidchars] = '\0';
			snprintf(fc_mainpath, sizeof(fc_mainpath), "%s/%s%s",
					 fc_dbspacedirname, fc_oidbuf, fc_de->d_name + fc_oidchars + 1 +
					 strlen(forkNames[INIT_FORKNUM]));

			fsync_fname(fc_mainpath, false);
		}

		FreeDir(fc_dbspace_dir);

		/* 
		 * 最后，fsync 数据库目录本身，确保
		 * 文件系统记住我们所做的文件创建和删除。
		 * 在仅进行 UNLOGGED_RELATION_CLEANUP 的调用中，我们并不在乎这个，
		 * 因为如果在我们进行 UNLOGGED_RELATION_INIT 之前恢复崩溃，
		 * 我们将在下一次启动尝试中重新执行清理步骤。
		 */
		fsync_fname(fc_dbspacedirname, true);
	}
}

/*
 * 对假定关系文件名进行基本解析。
 *
 * 如果文件似乎符合非临时关系的正确格式，则此函数返回 true，
 * 否则返回 false。
 *
 * 注意：如果此函数返回 true，调用者有权假定
 * *oidchars 已设置为不超过 OIDCHARS 的值，因此
 * OIDCHARS + 1 个字符的缓冲区足以容纳文件名的 OID
 * 部分。这对于防止可能的缓冲区溢出至关重要。
 */
bool parse_filename_for_nontemp_relation(const char *fc_name, int *fc_oidchars,
									ForkNumber *fc_fork)
{
	int			fc_pos;

	/* 寻找非空数字字符串（不能太长）。 */
	for (fc_pos = 0; isdigit((unsigned char) fc_name[fc_pos]); ++fc_pos)
		;
	if (fc_pos == 0 || fc_pos > OIDCHARS)
		return false;
	*fc_oidchars = fc_pos;

	/* 检查分叉名称。 */
	if (fc_name[fc_pos] != '_')
		*fc_fork = MAIN_FORKNUM;
	else
	{
		int			fc_forkchar;

		fc_forkchar = forkname_chars(&fc_name[fc_pos + 1], fc_fork);
		if (fc_forkchar <= 0)
			return false;
		fc_pos += fc_forkchar + 1;
	}

	/* 检查段号。 */
	if (fc_name[fc_pos] == '.')
	{
		int			fc_segchar;

		for (fc_segchar = 1; isdigit((unsigned char) fc_name[fc_pos + fc_segchar]); ++fc_segchar)
			;
		if (fc_segchar <= 1)
			return false;
		fc_pos += fc_segchar;
	}

	/* 现在我们应该处于末尾。 */
	if (fc_name[fc_pos] != '\0')
		return false;
	return true;
}
